﻿///
/// Copyright (c) 2008, Aaron G. Daisley-Harrison
/// All rights reserved.
/// 
/// Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
/// 
/// * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
/// 
/// * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
/// 
/// * Neither the name of Daisley-Harison Software nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.
/// 
/// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING,
/// BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT
/// SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 
/// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, 
/// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS 
/// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, 
/// STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF 
/// THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 
/// 
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.IO;
using System.Windows.Media.Media3D;
using System.Windows.Media;
using System.Windows;
using System.Diagnostics;

namespace DaisleyHarrison.Wavefront
{
    /// <summary>
    /// Support for reading Wavefront object files based on the specification found here:
    /// http://local.wasp.uwa.edu.au/~pbourke/dataformats/obj/
    /// 
    /// Note that at this time only a sub-set of the complete specific is supported.
    /// 
    /// </summary>
    public class ObjectLibrary
    {
        public const string TOKEN_COMMENT = "#";
        public const string TOKEN_VERTEX = "v";
        public const string TOKEN_PARAMETER_SPACE_VERTEX = "vp";
        public const string TOKEN_TEXTURE_COORDINATE = "vt";
        public const string TOKEN_VERTEX_NORMAL = "vn";
        public const string TOKEN_FACE = "f";
        public const string TOKEN_POINT = "p";
        public const string TOKEN_LINE = "l";
        public const string TOKEN_OBJECT = "o";
        public const string TOKEN_GROUP = "g";
        public const string TOKEN_MERGE_GROUP = "mg";
        public const string TOKEN_SMOOTH_GROUP = "s";
        public const string TOKEN_USE_MATERIAL = "usemtl";
        public const string TOKEN_MATERIAL_LIBRARY = "mtllib";
        public const string TOKEN_CALL = "call";

        private string _fileDirectory;
        private string _name;
        private List<Vertex> _vertices;
        private List<TextureCoordinate> _textureCoordinates;
        private List<ParameterSpaceVertex> _parameterSpaceVertices;
        private Vector3DCollection _vertexNormals;
        private Dictionary<int, int> _positionByIndex;
        private Dictionary<string, DefinedObject> _objects;
        private DefinedObject _object;
        private string _materialName;
        private DefinedMaterial _material;
        private Group _group;
        private List<string> _comments;

        public ObjectLibrary()
        {
        }
       public void Read(string filePath, params string[] args)
        {
            Parser parser = new Parser();
            initialize(parser);
            if (args != null)
            {
                for (int i = 0; i < args.Length; i++)
                {
                    parser.SetParemeter("$" + (i + 1), args[i]);
                }
            }
            _name = Path.GetFileName(filePath);
            _fileDirectory = Path.GetDirectoryName(filePath);
            parser.Parse(filePath);
        }
       public string Name
       {
           get
           {
               return _name;
           }
       }
        private void initialize(IParseContext context)
        {
            context.RegisterHandler(TOKEN_COMMENT, parseComment);
            context.RegisterHandler(TOKEN_VERTEX, parseVertex);
            context.RegisterHandler(TOKEN_PARAMETER_SPACE_VERTEX, parseParameterSpaceVertex);
            context.RegisterHandler(TOKEN_TEXTURE_COORDINATE, parseTextureCoordinate);
            context.RegisterHandler(TOKEN_VERTEX_NORMAL, parseVertexNormal);
            context.RegisterHandler(TOKEN_FACE, parseFace);
            context.RegisterHandler(TOKEN_POINT, parsePoint);
            context.RegisterHandler(TOKEN_LINE, parseLine);
            context.RegisterHandler(TOKEN_OBJECT, parseObject);
            context.RegisterHandler(TOKEN_GROUP, parseGroup);
            context.RegisterHandler(TOKEN_SMOOTH_GROUP, parseSmoothGroup);
            context.RegisterHandler(TOKEN_MERGE_GROUP, parseMergeGroup);
            context.RegisterHandler(TOKEN_USE_MATERIAL, parseUseMaterial);
            context.RegisterHandler(TOKEN_MATERIAL_LIBRARY, parseMaterialLibrary);
            context.RegisterHandler(TOKEN_CALL, parseCall);
            _vertices = new List<Vertex>();
            _parameterSpaceVertices = new List<ParameterSpaceVertex>();
            _textureCoordinates = new List<TextureCoordinate>();
            _vertexNormals = new Vector3DCollection();
            _positionByIndex = new Dictionary<int, int>();
            _objects = new Dictionary<string, DefinedObject>();
            _object = null;
            _group = null;
        }
        public IEnumerable<MaterialLibrary> MaterialLibraries
        {
            get
            {
                return _materialLibraries;
            }
        }
        public IEnumerable<string> ObjectNames
        {
            get
            {
                return _objects.Keys.ToArray();
            }
        }
        public DefinedObject GetObject(string objectName)
        {
            return _objects[objectName];
        }

        public List<string> Comments
        {
            get
            {
                if (_comments == null)
                {
                    _comments = new List<string>();
                }
                return _comments;
            }
        }
        private DefinedObject CurrentObject
        {
            get
            {
                if (_object == null)
                {
                    if (!_objects.TryGetValue(DefinedObject.UNDEFINED, out _object))
                    {
                        _object = new DefinedObject(DefinedObject.UNDEFINED);
                        _objects.Add(DefinedObject.UNDEFINED, _object);
                    }
                }
                return _object;
            }
        }
        private Group CurrentGroup
        {
            get
            {
                if (_group == null)
                {
                    _group = this.CurrentObject.CurrentGroup;
                }
                return _group;
            }
        }
        private void parseComment(IParseContext context)
        {
            string comment = context.ParseToEndOfLine();
            if (comment.Length > 0)
            {
                this.Comments.Add(comment);
            }
        }

        private void parseVertex(IParseContext context)
        {
            try
            {
                double x = context.ParseDouble();
                double y = context.ParseDouble();
                double z = context.ParseDouble();
                double w = context.ParseDouble(1.0);
                _vertices.Add(new Vertex(x, y, z, w));
            }
            catch (Exception exception)
            {
                throw new InvalidCastException("Invalid vertex format", exception);
            }
        }
        private void parseTextureCoordinate(IParseContext context)
        {
            try
            {
                double u = context.ParseDouble();
                double v = context.ParseDouble(0.0);
                double w = context.ParseDouble(0.0);
                _textureCoordinates.Add(new TextureCoordinate(u, v, w));
            }
            catch (Exception exception)
            {
                throw new InvalidCastException("Invalid vertex format", exception);
            }
        }
        private void parseParameterSpaceVertex(IParseContext context)
        {
            try
            {
                double u = context.ParseDouble();
                double v = context.ParseDouble(double.NaN);
                double w = context.ParseDouble(1.0);
                _parameterSpaceVertices.Add(new ParameterSpaceVertex(u, v, w));
            }
            catch (Exception exception)
            {
                throw new InvalidCastException("Invalid parameter space vertex format", exception);
            }
        }
        private void parseVertexNormal(IParseContext context)
        {
            try
            {
                double i = context.ParseDouble();
                double j = context.ParseDouble();
                double k = context.ParseDouble();
                _vertexNormals.Add(new Vector3D(i, j, k));
            }
            catch (Exception exception)
            {
                throw new InvalidCastException("Invalid vertex normal format", exception);
            }
        }
        private const int INDEX_NOT_SPEICIFED = 0;
        private int parseIndex(string[] tokens, int index)
        {
            if (index >= tokens.Length)
            {
                return INDEX_NOT_SPEICIFED;
            }
            else
            {
                string token = tokens[index];
                int result;
                if (token.Length == 0)
                {
                    return INDEX_NOT_SPEICIFED;
                }
                else if (int.TryParse(token, out result))
                {
                    return result;
                }
                else
                {
                    throw new InvalidCastException("Invalid index format");
                }
            }
        }
        private void parseVertex(string buffer)
        {
            try
            {
                Group group = this.CurrentGroup;
                string[] elements = buffer.Split('/');
                int vertexIndex = parseIndex(elements, 0);
                if (vertexIndex == INDEX_NOT_SPEICIFED)
                {
                    throw new Exception("Vertex index not supplied");
                }
                else
                {
                    int textureVertexIndex = parseIndex(elements, 1);
                    int vertexNormalIndex = parseIndex(elements, 2);
                    if (vertexIndex < 0)
                    {
                        vertexIndex = _vertices.Count - vertexIndex;
                    }
                    else
                    {
                        vertexIndex -= 1; //1 based index
                    }
                    int position;
                    if (!_positionByIndex.TryGetValue(vertexIndex, out position))
                    {
                        position = group.Mesh.Positions.Count;
                        group.Mesh.Positions.Add(_vertices[vertexIndex].ToPoint3D());
                        _positionByIndex.Add(vertexIndex, position);
                    }
                    group.Mesh.TriangleIndices.Add(position);
                    if (vertexNormalIndex != INDEX_NOT_SPEICIFED)
                    {
                        if (vertexNormalIndex < 0)
                        {
                            vertexNormalIndex = _vertexNormals.Count - vertexNormalIndex;
                        }
                        else
                        {
                            vertexNormalIndex -= 1; //1 based index
                        }
                        group.Mesh.Normals.Add(_vertexNormals[vertexNormalIndex]);
                    }
                    if (textureVertexIndex != INDEX_NOT_SPEICIFED)
                    {
                        if (textureVertexIndex < 0)
                        {
                            textureVertexIndex = _textureCoordinates.Count - textureVertexIndex;
                        }
                        else
                        {
                            textureVertexIndex -= 1; //1 based index
                        }
                        group.Mesh.TextureCoordinates.Add(_textureCoordinates[textureVertexIndex].ToPoint());
                    }
                }
            }
            catch (Exception exception)
            {
                throw new InvalidCastException("Invalid face vertex format", exception);
            }
        }
        private void parseFace(IParseContext context)
        {
            try
            {
                string token = context.NextToken();

                parseVertex(token);

                token = context.NextToken();

                parseVertex(token);

                token = context.NextToken();

                parseVertex(token);

                token = context.PeekToken();

                while (token != TokenConstants.TOKEN_EOL && token != TokenConstants.TOKEN_EOF)
                {
                    token = context.NextToken();

                    parseVertex(token);

                    token = context.PeekToken();
                }
            }
            catch (Exception exception)
            {
                throw new ParseException(context.LineNumber, context.LinePosition, "Invalid face format", exception);
            }
        }
        private void parsePoint(IParseContext context)
        {
            try
            {
                string token = context.PeekToken();
                while (token != TokenConstants.TOKEN_EOL && token != TokenConstants.TOKEN_EOF)
                {
                    int vertexIndex = context.ParseInteger();
                    if (vertexIndex < 0)
                    {
                        vertexIndex = _vertices.Count - vertexIndex;
                    }
                    else
                    {
                        vertexIndex -= 1; //1 based index
                    }
                    Vertex vertex;
                    int position;
                    if (!_positionByIndex.TryGetValue(vertexIndex, out position))
                    {
                        position = _group.Mesh.Positions.Count;
                        vertex = _vertices[vertexIndex];
                    }
                    token = context.PeekToken();
                }
                Debug.WriteLine("Point parsing not fully implemented");
            }
            catch (Exception exception)
            {
                throw new ParseException(context.LineNumber, context.LinePosition, "Invalid point format", exception);
            }
        }
        private void parseLineVertex(string buffer)
        {
            try
            {
                Group group = this.CurrentGroup;
                string[] elements = buffer.Split('/');
                int vertexIndex = parseIndex(elements,0);
                if (vertexIndex == INDEX_NOT_SPEICIFED)
                {
                    throw new Exception("Vertex index not supplied");
                }
                else
                {
                    int textureVertexIndex = parseIndex(elements,1);
                    if (vertexIndex < 0)
                    {
                        vertexIndex = _vertices.Count - vertexIndex;
                    }
                    else
                    {
                        vertexIndex -= 1; //1 based index

                    }
                    Vertex vertex;
                    TextureCoordinate textureCoordiate;
                    int position;
                    if (!_positionByIndex.TryGetValue(vertexIndex, out position))
                    {
                        position = _group.Mesh.Positions.Count;
                        vertex = _vertices[vertexIndex];
                    }
                    if (textureVertexIndex != INDEX_NOT_SPEICIFED)
                    {
                        if (textureVertexIndex < 0)
                        {
                            textureVertexIndex = _textureCoordinates.Count - textureVertexIndex;
                        }
                        else
                        {
                            textureVertexIndex -= 1; //1 based index
                        }
                        textureCoordiate = _textureCoordinates[textureVertexIndex];
                    }
                }
            }
            catch (Exception exception)
            {
                throw new InvalidCastException("Invalid line vertex format", exception);
            }
        }
        private void parseLine(IParseContext context)
        {
            try
            {
                string token = context.PeekToken();
                while (token != TokenConstants.TOKEN_EOL && token != TokenConstants.TOKEN_EOF)
                {
                    token = context.NextToken();
                    parseLineVertex(token);
                    token = context.PeekToken();
                }
                Debug.WriteLine("Line parsing not fully implemented");
            }
            catch (Exception exception)
            {
                throw new ParseException(context.LineNumber, context.LinePosition, "Invalid point format", exception);
            }
        }
        private void parseObject(IParseContext context)
        {
            string objectName = context.ParseString("object-" + (_objects.Count + 1));
            _object = new DefinedObject(objectName);
            _objects.Add(objectName,_object);
        }
        private void parseGroup(IParseContext context)
        {
            string groupName = context.ParseString("group-" + (this.CurrentObject.Groups.Count + 1 ));
            _group = new Group(groupName, new MeshGeometry3D());
            _group.Material = _material;
            this.CurrentObject.Groups.Add(groupName,_group);
            this.CurrentObject.CurrentGroup = _group;
            _positionByIndex.Clear();
        }
        private void parseMergeGroup(IParseContext context)
        {
            parseGroup(context);
            _group.Type = Group.Types.Merge;
        }
        private void parseSmoothGroup(IParseContext context)
        {
            parseGroup(context);
            _group.Type = Group.Types.Smooth;
        }
        private void parseUseMaterial(IParseContext context)
        {
            _materialName = context.ParseString();
            _material = FindMaterial(_materialName);
            if (_material == null)
            {
                throw new ParseException(context.LineNumber, context.LinePosition,"Material \"" + _materialName + "\" was not found.");
            }
        }
        private List<MaterialLibrary> _materialLibraries;
        public DefinedMaterial FindMaterial( string materialName )
        {
                if (_materialLibraries != null)
                {
                    foreach( MaterialLibrary materialLibrary in _materialLibraries )
                    {
                        if (materialLibrary.Contains(materialName))
                        {
                            return materialLibrary[materialName];
                        }
                    }
                }
            return null;
        }
        private void parseMaterialLibrary(IParseContext context)
        {
            string libraryNameToken;
            do
            {
                libraryNameToken = context.NextToken();
                if (libraryNameToken != TokenConstants.TOKEN_EOL && libraryNameToken != TokenConstants.TOKEN_EOF)
                {
                    if (_fileDirectory != null)
                    {
                        libraryNameToken = Path.Combine(_fileDirectory, libraryNameToken);
                    }
                    MaterialLibrary materialLibarary = new MaterialLibrary();
                    materialLibarary.Read(libraryNameToken);
                    if (_materialLibraries == null)
                    {
                        _materialLibraries = new List<MaterialLibrary>();
                    }
                    _materialLibraries.Add(materialLibarary);
                }
            }
            while (libraryNameToken != TokenConstants.TOKEN_EOL && libraryNameToken != TokenConstants.TOKEN_EOF);
            
        }
        private void parseCall(IParseContext context)
        {
            string objectLibraryName = context.ParseString();
            if (_fileDirectory != null)
            {
                objectLibraryName = Path.Combine(_fileDirectory, objectLibraryName );
            }
            List<string> arguments = new List<string>();
            string token = context.PeekToken();
            while (token != TokenConstants.TOKEN_EOL && token != TokenConstants.TOKEN_EOF)
            {
                token = context.NextToken();
                arguments.Add(token);
            }
            ObjectLibrary calledLibrary = new ObjectLibrary();
            calledLibrary.Read(objectLibraryName, arguments.ToArray());
        }
        private const string TO_STRING_FORMAT = "ObjectLibrary{{ Name=\"{0}\", Objects={1}}}";
        public override string ToString()
        {
            return string.Format(TO_STRING_FORMAT, this.Name, _objects.Count);
        }
        ModelVisual3D _modelVisual3D;
        public ModelVisual3D BuildVisual()
        {
            if (_modelVisual3D == null)
            {
                if (_objects.Count == 1)
                {
                    foreach (DefinedObject object3D in _objects.Values)
                    {
                        _modelVisual3D.Content = object3D.BuildModel();
                    }
                    throw new InvalidOperationException();
                }
                else
                {
                    _modelVisual3D = new ModelVisual3D();
                    Model3DGroup model3DGroup = new Model3DGroup();
                    foreach (DefinedObject object3D in _objects.Values)
                    {
                        model3DGroup.Children.Add(object3D.BuildModel());
                    }
                    _modelVisual3D.Content = model3DGroup;

                }
            }
            return _modelVisual3D;
        }
    }
}
